﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;

namespace AppToolkit.WPF.UI.Search
{
    /// <summary>
    /// Encapsulates Searching a list of items.
    /// </summary>
    /// <typeparam name="TItem">The type of the item.</typeparam>
    public class SearchContext<TItem> : INotifyPropertyChanged
    {
        private IList<TItem> _resultSet;
        private int _currentIdx = 0;

        /// <summary>
        /// Initializes a new instance of the <see cref="SearchContext&lt;TItem&gt;"/> class.
        /// </summary>
        public SearchContext()
        {
            // reset cached result set in case SearchSpecification changes
            this.PropertyChanged += (s, a) =>
                {
                    if (string.CompareOrdinal(a.PropertyName, "SearchSpecification") == 0)
                    {
                        this._resultSet = null;
                    }
                };
        }

        #region Property SourceCollection

        private ICollection<TItem> _SourceCollection;

        /// <summary>
        /// Specifies the collection that shall be searched.
        /// </summary>
        public ICollection<TItem> SourceCollection
        {
            get
            {
                return _SourceCollection;
            }
            set
            {
                if (_SourceCollection != value)
                {
                    _SourceCollection = value;
                    OnPropertyChanged("SourceCollection");
                }
            }
        }

        #endregion Property SourceCollection

        #region ErrorHandling

        #region Property ErrorInfo

        private ErrorInfo _ErrorInfo;

        /// <summary>
        /// In case last search has resulted in an error, the type of error can be inferred from this property.
        /// </summary>
        public ErrorInfo ErrorInfo
        {
            get
            {
                return _ErrorInfo;
            }
            set
            {
                if (_ErrorInfo != value)
                {
                    _ErrorInfo = value;
                    OnPropertyChanged("ErrorInfo");
                    OnPropertyChanged("HasError");
                }
            }
        }

        #endregion Property ErrorInfo

        /// <summary>
        /// True, in case the last search has resulted in an error.
        /// </summary>
        public bool HasError { get { return _ErrorInfo != ErrorInfo.NoError; } }

        #endregion ErrorHandling

        #region Property SearchSpecification

        private string _SearchSpecification;

        /// <summary>
        /// Specifies the value to search for.
        /// </summary>
        public string SearchSpecification
        {
            get
            {
                return _SearchSpecification;
            }
            set
            {
                if (_SearchSpecification != value)
                {
                    _SearchSpecification = value;
                    OnPropertyChanged("SearchSpecification");
                }
            }
        }

        #endregion Property SearchSpecification

        /// <summary>
        /// Specify how to generate a filtered set of elements.
        /// Parameters are:
        /// SourceCollection, SearchSpecification
        /// Return:
        /// filtered set of elements
        /// </summary>
        public Func<IEnumerable<TItem>, string, IEnumerable<TItem>> FilterSetGeneration { get; set; }

        /// <summary>
        /// Gets called to trigger the selection of a specific element.
        /// </summary>
        public Action<TItem> SelectItem { get; set; }

        #region SearchNextCommand Implementation

        ICommand _searchNextCommand;

        /// <summary>
        /// Triggers searching for the next element complying with the <see cref="SearchSpecification"/>.
        /// </summary>
        public ICommand SearchNextCommand
        {
            get
            {
                if (_searchNextCommand == null)
                {
                    _searchNextCommand = new RelayCommand(ONSearchNext, ONCanSearchNext);
                }
                return _searchNextCommand;
            }
        }

        private bool ONCanSearchNext()
        {
            return isInitialized() &&
                   !string.IsNullOrEmpty(this.SearchSpecification);
        }

        private void ONSearchNext()
        {
            Contract.Requires<InitializationException>(isInitialized(), "SearchContext not initialized correctly!");
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(this.SearchSpecification), "No SearchSpecification set!");
            Trace.TraceInformation("Search forward ...");

            this.ErrorInfo = ErrorInfo.NoError;

            if (_resultSet == null)
            {
                _resultSet = this.FilterSetGeneration(this.SourceCollection, this.SearchSpecification).ToArray();
                _currentIdx = 0;
            }
            else
            {
                _currentIdx++;
            }

            int resultSetCount = _resultSet.Count;
            if (resultSetCount == 0)
            {
                this.ErrorInfo = ErrorInfo.ResultSetIsEmpty;
                return;
            }
            else if (_currentIdx >= resultSetCount)
            {
                this.ErrorInfo = ErrorInfo.LastItemReached;
                _currentIdx = resultSetCount;
                return;
            }

            this.SelectItem(_resultSet[_currentIdx]);
        }

        #endregion SearchNextCommand Implementation

        #region SearchPrevCommand Implementation

        ICommand _searchPrevCommand;

        /// <summary>
        /// Triggers searching for the previous element complying with the <see cref="SearchSpecification"/>.
        /// </summary>
        public ICommand SearchPrevCommand
        {
            get
            {
                if (_searchPrevCommand == null)
                {
                    _searchPrevCommand = new RelayCommand(ONSearchPrev, ONCanSearchPrev);
                }
                return _searchPrevCommand;
            }
        }

        private bool ONCanSearchPrev()
        {
            return isInitialized() &&
                   !string.IsNullOrEmpty(this.SearchSpecification);
        }

        private void ONSearchPrev()
        {
            Contract.Requires<InitializationException>(isInitialized(), "SearchContext not initialized correctly!");
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(this.SearchSpecification), "No SearchSpecification set!");
            Trace.TraceInformation("Search backwards ...");

            this.ErrorInfo = ErrorInfo.NoError;

            if (_resultSet == null)
            {
                _resultSet = this.FilterSetGeneration(this.SourceCollection, this.SearchSpecification).ToArray();
                _currentIdx = _resultSet.Count - 1;
            }
            else
            {
                _currentIdx--;
            }

            int resultSetCount = _resultSet.Count;
            if (resultSetCount == 0)
            {
                this.ErrorInfo = ErrorInfo.ResultSetIsEmpty;
                return;
            }
            else if (_currentIdx < 0)
            {
                this.ErrorInfo = ErrorInfo.FirstItemReached;
                _currentIdx = -1;
                return;
            }

            this.SelectItem(_resultSet[_currentIdx]);
        }

        #endregion SearchPrevCommand Implementation

        #region ResetCommand Implementation

        ICommand _resetCommand;

        /// <summary>
        /// Resets the <see cref="SearchSpecification"/> and the pointer to the current location.
        /// </summary>
        public ICommand ResetCommand
        {
            get
            {
                if (_resetCommand == null)
                {
                    _resetCommand = new RelayCommand(onReset, onCanReset);
                }
                return _resetCommand;
            }
        }

        private bool onCanReset()
        {
            return true;
        }

        private void onReset()
        {
            _resultSet = null;
            SearchSpecification = string.Empty;
            this.ErrorInfo = ErrorInfo.NoError;
        }

        #endregion ResetCommand Implementation

        #region Internals

        [Pure]
        private bool isInitialized()
        {
            return this.SourceCollection != null &&
                   this.FilterSetGeneration != null &&
                   this.SelectItem != null;
        }

        #endregion Internals

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Notify that property 'propertyName' has changed.
        /// </summary>
        /// <param name="propertyName">name of the property that has changed.</param>
        protected void OnPropertyChanged(string propertyName)
        {
            if (null != PropertyChanged)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion INotifyPropertyChanged Members
    }
}